昨天提到了要盡量拆開程式碼,把程式碼拆成商業邏輯與 Rive 邏輯,那商業邏輯或 Rive 邏輯內部之間又要怎麼拆,拆完要怎麼組合?就是今天要討論的問題,也就是怎麼組織程式碼的問題。
組織程式碼的目的之一,是為了提升程式碼的可讀性、可維護性、與可擴充性,基於這三個標準,組織程式碼的方法有物件導向、函式導向、事件導向三種。
不知道大家還記不記得國中生物學🙈。生物由細胞、組織、器官、系統、個體組成。可以把程式碼想像成細胞、我們的專案想像成個體,因此物件導向就像器官、函式導向就像組織、事件有點像系統。這不是一個很精確的比喻,但我覺得可以先這樣理解,會比較能體會接下來我想說的。
物件導向最大的優點是直觀。對人腦來說,函式與事件都是相對抽象的東西,以抽象的東西來組織程式碼,當然會比較難理解。其實這就是物件這個概念被發明的目的:讓人腦更好分類事物,而不用在意背後抽象的實作細節。
沒有物件來分類,其實就是函式導向,因此整份程式碼只會由很多小小的函式所組成,容易迷失在函式海裡面,這也是為什麼 FP 這麼難學的原因。大腦比較能理解具體的東西,也就是物,物體,物件,而不是抽象的概念。函式導向相對於物件導向,把程式碼更抽象化了一點,抽象化可以說是以可讀性與可維護性為代價,提升可復用性。但越面對使用者的應用,因為常常有客製化的需求,可復用性再怎麼樣也不會太高,因此為了少量的可復用性,犧牲大量的可讀性與可維護性,我個人是覺得有點得不償失。
如果物件導向與函式導向是切分粒度的問題,那事件導向就是切分維度的問題。事件因為是外部的刺激,所以如果以事件分類,那等於說是用外部分類內部,把外部的東西混進內部,提高耦合性。說實在不是不可以,但是既然可以用內部的方式分類,那當然會更好一點。而且不同的事件可能會用到同樣的物件或函式,所以如果以純事件導向分類的話,會有大量重複的部分,降低可復用性,這一點實務上尤其明顯,特別是在新手的程式碼裡面。
// Bad: 函式導向
const useKnightRive = () => {
// other codes...
return { setSkinValue, setAttackValue }
}
// Bad: 事件導向
const useKnightRive = () => {
// other codes...
handleSkinClick()
handleArrowClick()
handleSwordClick()
// 事件導向的另一個壞處是,很容易把 bussiness logic & Rive logic 寫在一起
// 畢竟事件通常是 bussiness 層處理的問題
}
// Good: 物件導向
const useKnightRive = () => {
// other codes...
return { changeSkin, shot, attack }
}
對我來說,物件導向是一個直觀的程式碼組織方式,有很高的可讀性與可維護性,可復用性也不錯。物件也是人腦認識世界的一種基本方式,這就是為什麼我選擇物件導向。
回到一開始的生物學比喻,如果我說「人體由血液、肌肉、骨頭、脂肪、水分組成」或是「人體由呼吸、循環、消化、運動系統組成」其實也不是不可以,只是比較難理解一點。但如果說「人體由眼睛、鼻子、嘴巴、手收、腳腳組成」,通常腦袋會更有畫面,這就是物件導向比較直觀的原因,人類天生就是透過物件認識世界的。
不過我必須承認,函式導向或事件導向也能寫出很好的程式碼,而且實務上這三者比較像是光譜,而不是黑白分明的,所以我還是覺得物件導向在大部分的情況比較好,但如果遇到偏好函式導向或事件導向的工程師,其實也很可以接受。